<?php
/**
 * Created by PhpStorm.
 * auth: Inhere
 * Date: 14-6-4
 * Time: 下午11:52
 */
namespace ulue\autoload;

class ClassLoader
{

    /**
     * 命名空间列表 namespace => path
     * @var string[]
     */
    private $npMap           = [];

    /**
     * 类加载路径列表 classname => path
     * @var string[]
     */
    private $classMap        = [];

    /**
     * 注册自定义加载回调
     * @var null|callback
     */
    private $callback        = null;

    /**
     * 类加载包含路径列表 [path1,path2]
     * @var string[]
     */
    private $includeMap      = [];

    // 备用目录
    private $fallbackDirPsr0;

    // private $fallbackDirPsr4;

    /**
     * psr4 命名空间列表 namespace => path
     * @var string[]
     */
    // private $psr4Map       = [];

    private function __clone()
    {}

    /**
     * [register description]
     * @param  callback     $callback
     * @return void
     */
    public function register($callback=null)
    {
        $callback = $callback ?: array($this, 'autoLoader');

        spl_autoload_register($callback);
    }

    /**
     * [unregister description]
     * @param  callback     $callback
     * @return voic
     */
    public function unregister($callback=null)
    {
        $callback = $callback ?: array($this, 'autoLoader');

        spl_autoload_unregister($callback);
    }

    /**
     * set 类加载包含路径列表
     * @param  array      $map
     */
    public function setIncludeMap(array $map)
    {
        if ($map) {
            $this->includeMap = $map;
        }
    }

    /**
     * set 命名空间列表
     * @param  array      $npMap
     */
    public function setNsMap(array $npMap)
    {
        foreach ($npMap as $np => $path) {
            $this->setNs($ns, $path);
        }
    }

    public function getNsMap()
    {
        return $this->npMap;
    }

    /**
     * 设置 系统命名空间=>文件夹 关系映射
     * @param string $namespace
     * @param string $path
     */
    public function setNs($namespace, $path)
    {
        if (!is_dir($path)) {
            trigger_error('需要命名空间对应的路径，传入 $path :' . $path, E_USER_NOTICE);
        }

        if (!$namespace) {
            $this->fallbackDirPsr0 = $path;
        } else {
            $this->npMap[$namespace] = $path;
        }
    }

    /**
     * 获取 命名空间对应的真实目录
     * @param  string $namespace
     * @return mixed
     */
    public function resolveNs($namespace)
    {
        $namespace = trim($namespace,'\\ ');
        $npMap     = $this->npMap;

        if ( isset($npMap[$namespace]) ) {
            return $npMap[$namespace];
        }

        // 子空间
        if ( strpos($namespace, '\\') ) {
            $last = $path = '';

            /**
             * 循环检查， 取最近的父级命名空间所在目录
             * @example
             * @param $namespace = 'admin\test\sub\np';
             * @internal $npMap = [
             *     ... ...
             *     'admin' => "J:/webroot/phpTest/blog/src/admin"
             *     'admin\test' => "J:/webroot/phpTest/blog/src/admin/modules/test"
             *     ... ...
             * ]
             * ==> 'admin\test\sub\np' 最近的父级命名空间为  'admin\test' 。
             * 则以 $npMap['admin\test'] 所在路径来查找 'admin\test\sub\np' 所在路径，
             * 而不是使用 $npMap['admin'] 的路径为基准来查找             *
             */
            while ( $namespace && $namespace!= '.' ) {
                $last      = basename($namespace) . '/' . $last;
                $namespace = dirname($namespace);

                if ( isset($npMap[$namespace]) ) {
                    $path = $npMap[$namespace];
                    break;
                }
            }

            return !$path ? false : $path . '/' . trim($last,'/');
        }

        return false;
    }

    public function hasNs($namespace)
    {
        return isset($this->npMap[$namespace]);
    }

    public function addClassMap(array $map)
    {
        if ($this->classMap) {
            $this->classMap = array_merge($this->classMap,$map);
        } else {
            $this->classMap = $map;
        }
    }

    public function getClassMap()
    {
       return $this->classMap;
    }

    /**
     * 添加类库自动加载记录: 特殊路径的类、无命名空间类(顶级类)自动加载;
     * @param string $className
     * @param string $path
     */
    public function addClass($className, $path)
    {
        if (!$className) {
            throw new \Exception("请传入要设置加载的类名称！！");
        }

        # 设置一个 类关系映射
        if (is_string($className)) {
            if (!$path) {
                throw new \Exception("请传入要设置加载的类所在路径！！");
            }

           $this->classMap[$className] = $path;
        }
    }

    public function getClass($className='')
    {
        if (!$className) {
            return $this->classMap;
        }

        return isset($this->classMap[$className]) ? $this->classMap[$className] : null;
    }

    public function setCallback($callback)
    {
        $this->callback = $callback;
    }

    /**
     * 自动加载
     * @example admin\model\User ==> @app.module.admin.model.User
     *      $namespace == admin\model
     *      $className == User
     *      $path      == @app.module.admin.model
     *      $classFile == @app.module.admin.model.User
     * @param  string $class (含有命名空间的)类名
     * @return void
     */
    public function autoLoader($class)
    {
        $class = trim($class,'\\ ');

        // has namespace
        if (false !== strpos($class,'\\')) {
            $classFile = $this->findHasNsClassFile($class);

            $this->requireFile($classFile,' 没有找到需要的类文件！');

        // only class name, top class
        } else {
            if ( $classPath = $this->getClass($class)) {
                $this->requireFile($classPath,'不存在。类路径关系映射可能有误！！');

            // custom path
            } else if ( $classFile = $this->findIncludePathClassFile($class) ) {
                $this->requireFile($classFile);

            // custom callback load
            } else if (
                !empty($this->callback) &&
                is_callable( $this->callback ) &&
                ($classFile = call_user_func($this->callback, $class)) != false
            ) {
                $this->requireFile($classFile);
            // } else {
            //     trigger_error('没有找到需要的类：[ '.$class.' ]', E_USER_ERROR);
            }
        }
    }

    /**
     * [findHasNsClassFile 查找有命名空间的类文件]
     * @param  string $class 类名称
     * @throws ulue\loader\LoadException
     * @return string
     */
    public function findHasNsClassFile($class)
    {
        $namespace      = dirname($class);
        $className      = basename($class);

        if ( !($path = $this->resolveNs($namespace)) ) {
            /**
             * 若设置了默认目录
             * 应用目录下的其他模块( 文件夹名) 默认以文件夹为命名空间
             * user/controllers/LoginController : src/user/controllers/LoginController.php
             */
            $default = $this->resolveNs('__default__') ?: $this->resolveNs('');

            if ( $default ) {
                $path  = str_replace( '\\', '/', $default .'/'. $namespace );

            } else {
                throw new LoadException( "没有找到命名空间[ {$class} ]的对应配置，请设置相应的命名空间关系映射！在 ".
                    __DIR__."/autoload_namespace.php " );
            }
        }

        if ( !file_exists( $path ) ) {
            throw new LoadException( sprintf("命名空间[ %s ]的指向目录：[ %s ]不存在！",$class,$path) );
        }

        if ( !$this->hasNs($namespace) ) {
            $this->setNs($namespace,$path);
        }

        return $path .'/'. $className . '.php';
    }

    /**
     * findIncludePathClassFile 查找指定目录的无命名空间类文件 @see __DIR__.'/include_path.php'
     * @param  string $class 类名称
     * @return bool|string
     */
    public function findIncludePathClassFile($class)
    {
        foreach ($this->includeMap as $path ) {

            if ( file_exists( $classFile = "{$path}/{$class}.php") ) {
                return $classFile;
            }
        }

        return false;
    }

    /**
     * require 调整
     * @param $file
     * @param string $errorMsg
     * @throws LoadException
     * @return mixed
     */
    public function requireFile($file, $errorMsg='')
    {
        if (!$file) {
            throw new LoadException( '请传入要加载的文件！！');
        }

        if ( is_array($file) ) {
            foreach($file as $v) {
                $this->requireFile($v);
            }
        } else {
            $filePath  = str_replace('\\', '/', trim($file));

            // 默认加载php文件时。可省略 .php
            if (strpos($filePath, '.')===false) {
                $filePath   = $filePath.'.php';
            }

            if ( !file_exists($filePath) ) {
                $errorMsg = empty($errorMsg) ? ' 不存在文件:' : $errorMsg;
                throw new LoadException( '目录: '.dirname($filePath).$errorMsg.basename($filePath));
            }

            // windows环境下检测文件大小写
            if ( ! $this->checkFileNameCase($filePath) ) {
                throw new LoadException(sprintf('请更正文件名 [%s] 大小写！以正确匹配文件。',basename($filePath)));
            }

            require $filePath;
        }
    }


    /**
     * 判断文件名大小写是否正确
     * @param string $file  需要判断的文件
     * @return boolean
     */
    public function checkFileNameCase($file)
    {
        // if (is_file($file)) {
            //windows环境下检测文件大小写
            if ( strstr(PHP_OS, 'WIN') ) {
                if (basename(realpath($file)) != basename($file)) {
                    return false;
                }
            }

            return true;
        // }

        // return false;
    }

}